#include "QtInteraction.hpp"

#include "QtConnectionGraphicsObject.hpp"
#include "QtNodeGraphicsObject.hpp"
#include "QtNodeDataModel.hpp"
#include "QtDataModelRegistry.hpp"
#include "QtFlowScene.hpp"

namespace Qt
{
	QtInteraction::
		QtInteraction(QtNode& node, QtConnection& connection, QtFlowScene& scene)
		: _node(&node)
		, _connection(&connection)
		, _scene(&scene)
	{}


	bool
		QtInteraction::
		canConnect(PortIndex &portIndex, TypeConverter & converter) const
	{
		// 1) QtConnection requires a port

		PortType requiredPort = connectionRequiredPort();


		if (requiredPort == PortType::None)
		{
			return false;
		}

		// 1.5) Forbid connecting the node to itself
		QtNode* nodeStart = _connection->getNode(oppositePort(requiredPort));

		if (nodeStart == _node)
			return false;

		// 2) connection point is on top of the node port

		QPointF connectionPoint = connectionEndScenePosition(requiredPort);

		portIndex = nodePortIndexUnderScenePoint(requiredPort,
			connectionPoint);

		if (portIndex == INVALID_PORT)
		{
			return false;
		}

		// 3) QtNode port is vacant
		if (!isNodePortAccessible(requiredPort, portIndex))
			return false;

		QtNode* nodeExp = requiredPort == PortType::In ? nodeStart : _node;
		QtNode* nodeInp = requiredPort == PortType::In ? _node : nodeStart;

		PortIndex portIndexExp = requiredPort == PortType::In ? _connection->getPortIndex(oppositePort(requiredPort)) : portIndex;
		PortIndex portIndexInp = requiredPort == PortType::In ? portIndex : _connection->getPortIndex(oppositePort(requiredPort));

		auto dataOut = nodeExp->nodeDataModel()->outData(portIndexExp);

		if (!nodeInp->nodeDataModel()->tryInData(portIndexInp, dataOut)) {
			return false;
		}

		// 4) QtConnection type equals node port type, or there is a registered type conversion that can translate between the two

		auto connectionDataType =
			_connection->dataType(oppositePort(requiredPort));

		auto const   &modelTarget = _node->nodeDataModel();
		NodeDataType candidateNodeDataType = modelTarget->dataType(requiredPort, portIndex);

		if (connectionDataType.id != candidateNodeDataType.id)
		{
			if (requiredPort == PortType::In)
			{
				converter = _scene->registry().getTypeConverter(connectionDataType, candidateNodeDataType);
			}
			else if (requiredPort == PortType::Out)
			{
				converter = _scene->registry().getTypeConverter(candidateNodeDataType, connectionDataType);
			}

			return (converter != nullptr);
		}

		return true;
	}


	bool
		QtInteraction::
		tryConnect() const
	{
		// 1) Check conditions from 'canConnect'
		PortIndex portIndex = INVALID_PORT;

		TypeConverter converter;

		if (!canConnect(portIndex, converter))
		{
			return false;
		}

		// 1.5) If the connection is possible but a type conversion is needed,
		//      assign a convertor to connection
		if (converter)
		{
			_connection->setTypeConverter(converter);
		}

		// 2) Assign node to required port in QtConnection
		PortType requiredPort = connectionRequiredPort();
		_node->nodeState().setConnection(requiredPort,
			portIndex,
			*_connection);

		// 3) Assign QtConnection to empty port in NodeState
		// The port is not longer required after this function
		_connection->setNodeToPort(*_node, requiredPort, portIndex);

		// 4) Adjust QtConnection geometry

		_node->nodeGraphicsObject().moveConnections();

		// 5) Poke model to intiate data transfer

		auto outNode = _connection->getNode(PortType::Out);
		if (outNode)
		{
			PortIndex outPortIndex = _connection->getPortIndex(PortType::Out);
			outNode->onDataUpdated(outPortIndex);
		}

		return true;
	}


	/// 1) QtNode and QtConnection should be already connected
	/// 2) If so, clear QtConnection entry in the NodeState
	/// 3) Set QtConnection end to 'requiring a port'
	bool
		QtInteraction::
		disconnect(PortType portToDisconnect) const
	{
		PortIndex portIndex =
			_connection->getPortIndex(portToDisconnect);

		NodeState &state = _node->nodeState();

		// clear pointer to QtConnection in the NodeState
		std::unordered_map<QUuid, QtConnection*>& connections = state.getEntries(portToDisconnect)[portIndex];
		auto iter = connections.begin();
		for (; iter != connections.end(); iter++)
		{
			if (iter->second == _connection) {
				connections.erase(iter);
				break;
			}
		}

		// 4) Propagate invalid data to IN node
		_connection->propagateDisconnectedData();

		// clear QtConnection side
		_connection->clearNode(portToDisconnect);

		_connection->setRequiredPort(portToDisconnect);

		_connection->getConnectionGraphicsObject().grabMouse();

		return true;
	}


	// ------------------ util functions below

	PortType
		QtInteraction::
		connectionRequiredPort() const
	{
		auto const &state = _connection->connectionState();

		return state.requiredPort();
	}


	QPointF
		QtInteraction::
		connectionEndScenePosition(PortType portType) const
	{
		auto &go =
			_connection->getConnectionGraphicsObject();

		ConnectionGeometry& geometry = _connection->connectionGeometry();

		QPointF endPoint = geometry.getEndPoint(portType);

		return go.mapToScene(endPoint);
	}


	QPointF
		QtInteraction::
		nodePortScenePosition(PortType portType, PortIndex portIndex) const
	{
		NodeGeometry const &geom = _node->nodeGeometry();

		QPointF p = geom.portScenePosition(portIndex, portType);

		QtNodeGraphicsObject& ngo = _node->nodeGraphicsObject();

		return ngo.sceneTransform().map(p);
	}


	PortIndex
		QtInteraction::
		nodePortIndexUnderScenePoint(PortType portType,
			QPointF const & scenePoint) const
	{
		NodeGeometry const &nodeGeom = _node->nodeGeometry();

		QTransform sceneTransform =
			_node->nodeGraphicsObject().sceneTransform();

		PortIndex portIndex = nodeGeom.checkHitScenePoint(portType,
			scenePoint,
			sceneTransform);
		return portIndex;
	}


	bool
		QtInteraction::
		isNodePortAccessible(PortType portType, PortIndex portIndex) const
	{
		NodeState const & nodeState = _node->nodeState();

		auto const & entries = nodeState.getEntries(portType);

		if (entries[portIndex].empty()) return true;

		if (portType == PortType::Out)
		{
			const auto outPolicy = _node->nodeDataModel()->portOutConnectionPolicy(portIndex);
			return outPolicy == QtNodeDataModel::ConnectionPolicy::Many;
		}
		else
		{
			const auto inPolicy = _node->nodeDataModel()->portInConnectionPolicy(portIndex);
			return inPolicy == QtNodeDataModel::ConnectionPolicy::Many;
		}
	}
}